﻿//Copyright (c) Microsoft Corporation.  All rights reserved.

using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage.Table.DataServices;
using System;
using System.Collections.Generic;
using System.Data.Services.Client;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading;

namespace Microsoft.Ted.Wacel.TableStorage
{
    public class CloudTableStorage<E> where E : new()
    {
        private CloudTableClient mClient;
        private CloudTable mTable;
        private PropertyInfo mIdField;
        private PropertyInfo mPartitionField;

        private bool mThrowExceptionWhenNotFound;

        public CloudTableStorage(string idFieldName, string connectionString, string tableName, bool notfoundException = true)
            :this(idFieldName,idFieldName, connectionString, tableName, notfoundException)
        {
        }
        public CloudTableStorage(string partitionFieldName, string idFieldName, string connectionString, string tableName, bool notfoundException = true)
        {
            mThrowExceptionWhenNotFound = notfoundException;

            Type type = typeof(E);
            mIdField = type.GetProperties().FirstOrDefault(f => f.Name == idFieldName);
            if (mIdField == null)
                throw new ArgumentException("Attribute not found on object.", idFieldName);

            mPartitionField = type.GetProperties().FirstOrDefault(f => f.Name == partitionFieldName);
            if (mPartitionField == null)
                throw new ArgumentException("Attribute not found on object.", partitionFieldName);

            CloudStorageAccount account = CloudStorageAccount.Parse(connectionString);
            mClient = account.CreateCloudTableClient();
            mTable = mClient.GetTableReference(tableName);
            try
            {
                mTable.CreateIfNotExists();
            }
            catch (StorageException exp)
            {
                if (exp.RequestInformation.HttpStatusCode != (int)HttpStatusCode.Conflict)
                    throw;
            }
        }

        public virtual void Clear()
        {
            mTable.DeleteIfExists();
            while (true)
            {
                try
                {
                    mTable.CreateIfNotExists();
                    return;
                }
                catch (StorageException exp)
                {
                    if (exp.RequestInformation.HttpStatusCode != (int)HttpStatusCode.Conflict)
                        throw;
                    else
                        Thread.Sleep(1000);
                }
            }
        }

        private AutoDynamicTableEntity<E> makeEntity(E entity)
        {
            var partitionProperty = mPartitionField.GetValue(entity);
            string partition = partitionProperty == null ? null : partitionProperty.ToString();
            var rowProperty = mIdField.GetValue(entity);
            string row = rowProperty == null ? null : rowProperty.ToString();
            if (string.IsNullOrEmpty(row) && string.IsNullOrEmpty(partition))
            {
                row = "K" + Guid.NewGuid().ToString("N");
                partition = row;
            }
            else if (string.IsNullOrEmpty(row))
                row = "K" + Guid.NewGuid().ToString("N");
            else if (string.IsNullOrEmpty(partition))
                partition = row;
            return new AutoDynamicTableEntity<E>(entity, partition, row, mPartitionField.Name, mIdField.Name);
        }
        public virtual string Insert(E entity)
        {
            TableOperation operation = TableOperation.Insert(makeEntity(entity));
            var result = mTable.Execute(operation);
            return result.Etag;
        }
        public virtual void InsertOrReplace(E entity)
        {
            TableOperation operation = TableOperation.InsertOrReplace(makeEntity(entity));
            mTable.Execute(operation);
        }
        public virtual void BulkInsertOrReplace(IEnumerable<E> entities)
        {
            TableBatchOperation batch = new TableBatchOperation();
            if (entities.Count() > 0)
            {
                foreach (var entity in entities)
                    batch.InsertOrReplace(makeEntity(entity));
                mTable.ExecuteBatch(batch);
            }
        }
        public virtual void InsertOrMerge(E entity)
        {
            TableOperation operation = TableOperation.InsertOrMerge(makeEntity(entity));
            mTable.Execute(operation);
        }
        public virtual string Replace(E entity)
        {
            var newEntity = makeEntity(entity);
            newEntity.ETag = "*";
            TableOperation operation = TableOperation.InsertOrReplace(newEntity);
            var result = mTable.Execute(operation);
            return result.Etag;
        }
        public virtual string Update(E entity, string Etag)
        {
            var newEntity = makeEntity(entity);
            newEntity.ETag = Etag;
            TableOperation operation = TableOperation.Merge(newEntity);
            var result = mTable.Execute(operation);
            return result.Etag;
        }
        public virtual void Delete(string id, string partitionId = null)
        {
            if (string.IsNullOrEmpty(id))
                throw new ArgumentNullException("Id can't be null.");
            if (string.IsNullOrEmpty(partitionId))
                partitionId = id;

            TableOperation readOperation = TableOperation.Retrieve<AutoDynamicTableEntity<E>>(partitionId, id);
            TableResult readResult = mTable.Execute(readOperation);
            if (readResult.Result != null)
            {
                TableOperation operation = TableOperation.Delete((AutoDynamicTableEntity<E>)readResult.Result);
                mTable.Execute(operation);
            }
        }
        public virtual E Read(string id, string partitionId = null)
        {
            string Etag = null;
            return Read(id, out Etag, partitionId);
        }
        public virtual E Read(string id, out string Etag, string partitionId = null)
        {
            if (string.IsNullOrEmpty(id))
                throw new ArgumentNullException("Id can't be null.");
            if (string.IsNullOrEmpty(partitionId))
                partitionId = id;
        
            TableOperation readOperation = TableOperation.Retrieve<AutoDynamicTableEntity<E>>(partitionId, id);
            TableResult readResult = mTable.Execute(readOperation);
            if (readResult.Result != null)
            {
                Etag = readResult.Etag;
                var ret = (AutoDynamicTableEntity<E>)readResult.Result;
                setKey(ret.POCO, mPartitionField, ret.PartitionKey);
                setKey(ret.POCO, mIdField, ret.RowKey);
                return ret.POCO;
            }
            else
            {
                if (mThrowExceptionWhenNotFound)
                    throw new ArgumentOutOfRangeException(string.Format("Entity with parition:'{0}', row:'{1}' not found.", partitionId, id));
                else
                {
                    Etag = "";
                    return default(E);
                }
            }
        }
        public virtual IEnumerable<E> List(string startId, string endId, string startPartitionId = null, string endPartitionId = null)
        {
            if (string.IsNullOrEmpty(startId))
                throw new ArgumentNullException("startId can't be null.");
            if (string.IsNullOrEmpty(endId))
                throw new ArgumentNullException("endId can't be null.");
            if (string.IsNullOrEmpty(startPartitionId) && !string.IsNullOrEmpty(endPartitionId)
                || !string.IsNullOrEmpty(startPartitionId) && string.IsNullOrEmpty(endPartitionId))
                throw new ArgumentNullException("Partition id filters have to be used in pairs.");


            TableQuery<AutoDynamicTableEntity<E>> query;
            string startPartitionFilter = null;
            string endPartitionFilter = null;
            string startIdFilter = null;
            string endIdFilter = null;

            if (ArePartitionKeyRowKeyDifferent)
            {
                string partitionFilter = null;
                if (!string.IsNullOrEmpty(startPartitionId))
                {
                    startPartitionFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.GreaterThanOrEqual, startPartitionId);
                    endPartitionFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.LessThanOrEqual, endPartitionId);
                    partitionFilter  = TableQuery.CombineFilters(startPartitionFilter, TableOperators.And, endPartitionFilter);
                }
                startIdFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.GreaterThanOrEqual, startId);
                endIdFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.LessThanOrEqual, endId);
                string rowFilter = TableQuery.CombineFilters(startIdFilter, TableOperators.And, endIdFilter);
                if (!string.IsNullOrEmpty(partitionFilter))
                    rowFilter = TableQuery.CombineFilters(rowFilter, TableOperators.And, partitionFilter);
                query = new TableQuery<AutoDynamicTableEntity<E>>().Where(rowFilter);
            }
            else
            {
                startIdFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.GreaterThanOrEqual, startId);
                endIdFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.LessThanOrEqual, endId);
                string rowFilter = TableQuery.CombineFilters(startIdFilter, TableOperators.And, endIdFilter);
                query = new TableQuery<AutoDynamicTableEntity<E>>().Where(rowFilter);
            }

            List<E> ret = new List<E>();

            foreach (AutoDynamicTableEntity<E> entity in mTable.ExecuteQuery(query))
            {
                setKey(entity.POCO, mPartitionField, entity.PartitionKey);
                setKey(entity.POCO, mIdField, entity.RowKey);
                ret.Add(entity.POCO);
            }

            return ret;
        }
        public virtual IEnumerable<E> List(Func<E, bool> predicate, string partitionId = null)
        {
            TableQuery<AutoDynamicTableEntity<E>> query;
            if (string.IsNullOrEmpty(partitionId))
                query = new TableQuery<AutoDynamicTableEntity<E>>();
            else
                query = new TableQuery<AutoDynamicTableEntity<E>>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionId));
            
            List<E> ret = new List<E>();

            foreach (AutoDynamicTableEntity<E> entity in mTable.ExecuteQuery(query))
            {
                setKey(entity.POCO, mPartitionField, entity.PartitionKey);
                setKey(entity.POCO, mIdField, entity.RowKey);
                if (predicate == null || predicate(entity.POCO))
                {
                    ret.Add(entity.POCO);
                }
            }

            return ret;
        }
        public  virtual IEnumerable<E> List(string partitionId = null)
        {
            return this.List(null, partitionId);
        }
        private void setKey(E entity, PropertyInfo property, string value)
        {
            switch (property.PropertyType.Name)
            {
                case "Int32":
                    property.SetValue(entity, int.Parse(value));
                    break;
                case "Int64":
                    property.SetValue(entity, Int64.Parse(value));
                    break;
                case "Guid":
                    property.SetValue(entity, Guid.Parse(value));
                    break;
                default:
                    property.SetValue(entity, value);
                    break;
            }
        }
        protected string GetRowKey(E entity)
        {
            return mIdField.GetValue(entity).ToString();
        }
        protected string GetPartitionKey(E entity)
        {
            return mPartitionField.GetValue(entity).ToString();
        }
        protected bool ArePartitionKeyRowKeyDifferent
        {
            get
            {
                return mIdField.Name != mPartitionField.Name;
            }
        }
    }
}
